Next.js에서 환경변수 다루기

React.js와 Next.js 모두 환경변수를 사용할 수 있습니다. 본 글에서는 Next.js 개발 환경을 전제로 설명하겠습니다. React.js도 동일하게 진행하시면 됩니다.

기존 문제점

Next.js에서 환경변수를 사용할 때 다음과 같은 문제가 발생합니다.

  • 정확한 타입 추론이 어려움
  • 불필요한 코드 추가 필요

대표적으로 위 2가지의 문제가 있습니다. 하나씩 알아보도록 하겠습니다.

타입 추론 불가 및 불필요한 처리

TS
// string | undefined
const key = process.env.key;

위와 같이 사용할 경우 process.env.keystring | undefined 타입이 됩니다. 개발자가 해당 값이 항상 존재한다고 확신하더라도, 타입 상 undefined를 제거해주어야 합니다. 그래서 다음과 같은 불필요한 처리를 하게 됩니다.

TS
const key1 = process.env.key as string;
const key2 = process.env.key || "";

as string이나 || ""는 불필요할 뿐 아니라 타입 안전성도 떨어뜨릴 수 있습니다.

유니언 타입이지만 추론 불가

TS
type Country = "Korean" | "Japan" | "China";
const country = process.env.country;

country 값이 반드시 "Korea" | "Japan" | "China" 중 하나여야 한다고 해도, 타입스크립트는 이를 추론하지 못합니다.

TS
const country1 = process.env.country as Country;
 
const country2 = process.env.country;
if (isCountry(country2)) {
  // do something ...
}

이처럼 커스텀 타입 가드나 타입 단언을 사용해야 해서 코드가 복잡해지고 유지보수가 어려워집니다.

해결방법

빌드타임 + 런타임 유효성 검사를 통해 환경변수를 안전하게 관리할 수 있습니다. 이를 위해 타입스크립트와 잘 어울리는 zod 라이브러리를 사용하였습니다. 해당 라이브러리를 설치해주셔야 진행 가능합니다.

환경 변수 조건 예시

다음과 같은 조건이 있다고 가정해보겠습니다.

  • key : 빈 문자열이 아닌 문자열
  • api : URL 형식의 문자열
  • count : 숫자
  • country : "Korea", "Japan", "China" 중 하나

zod 스키마 정의

환경변수에 대한 스키마를 구현해주어야 합니다.
스키마는 데이터 구조라고 생각해주시면 됩니다.

TS
import { z } from "zod";
 
const schema = z.object({
  key: z.string().nonempty("key 값은 필수 값입니다."),
  api: z
    .string()
    .nonempty("api 값은 필수 값입니다.")
    .url()
    .refine((val: string) => /\.[a-z]+$/.test(new URL(val).hostname), {
      message: "api 값이 유효한 도메인이 아님",
    }),
  count: z.number(),
  country: z.enum(["Korea", "Japan", "China"] as const, {
    errorMap: () => ({ message: "country 값이 유효하지 않음" }),
  }),
});

위 코드를 확인해보면 추가적인 설명 없이 충분히 이해할 수 있습니다. 이처럼 zod를 이용하면 객체의 필드에 대한 유효성 조건을 명확하게 설정할 수 있고 관리도 편하며 가독성도 높아집니다.

환경변수 객체

TS
const envObject = {
  key: process.env.NEXT_PUBLIC_KEY,
  api: process.env.NEXT_PUBLIC_API,
  count: Number(process.env.NEXT_PUBLIC_COUNT),
  country: process.env.NEXT_PUBLIC_COUNTRY,
};

유효성 검사를 하기위한 환경변수 객체를 생성해주어야 합니다.

유효성 검사 및 타입 완성된 환경변수 반환

TS
const validEnv = () => {
  const result = schema.safeParse(envObject);
 
  if (!result.success) {
    console.error(result.error.format());
    throw new Error("Error");
  } else {
    return result.data;
  }
};
 
const env = validEnv();

성공 시 env 객체는 정확한 타입이 지정된 상태로 사용 가능합니다. 따라서 자동완성도 잘 동작합니다.

Image 1

만약 잘못된 값을 넣으면 어떻게 될까요? 나머지 값은 잘 들어갔고 아래 2개의 값이 잘못 들어갔다고 가정해보겠습니다.

NEXT_PUBLIC_KEY=""
NEXT_PUBLIC_COUNTRY="Korea2"

다음과 같은 에러 메시지가 발생하게 됩니다.

TS
{
  key: { _errors: [ 'key 값은 필수입니다.' ] },
  country: { _errors: [ 'country 값이 유효하지 않습니다.' ] }
}

따라서 위 코드를 빌드타임에서 유효성 검사를 진행하여 환경변수에 잘못된 값이 있는 지 확인하는 것이 중요합니다.

전체 코드

TS
import { z } from "zod";
 
const schema = z.object({
  key: z.string().nonempty("key 값은 필수 값입니다."),
  api: z
    .string()
    .nonempty("api 값은 필수 값입니다.")
    .url()
    .refine((val: string) => /\.[a-z]+$/.test(new URL(val).hostname), {
      message: "api 값이 유효한 도메인이 아님",
    }),
  count: z.number(),
  country: z.enum(["Korea", "Japan", "China"] as const, {
    errorMap: () => ({ message: "country 값이 유효하지 않음" }),
  }),
});
 
const envObject = {
  key: process.env.NEXT_PUBLIC_KEY,
  api: process.env.NEXT_PUBLIC_API,
  count: process.env.NEXT_PUBLIC_COUNT,
  country: process.env.NEXT_PUBLIC_COUNTRY,
};
 
const validEnv = () => {
  const result = schema.safeParse(envObject);
 
  if (!result.success) {
    console.error(result.error.format());
    throw new Error("Error");
  } else {
    return result.data;
  }
};
 
const env = validEnv();

결론

이번 게시글에서는 환경변수 유효성 검사와 타입 설정에 대해 알아보았습니다. 빌드타임, 런타임 각각 실행하게 되면 사전에 잘못된 데이터를 막을 수 있고 타입도 자동완성이 되기 때문에 개발 효율성이 증가하게 될 것입니다.